/**
 *  Copyright 2013 University Pierre & Marie Curie - UMR CNRS 7606 (LIP6/MoVe)
 *  All rights reserved.   This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  Initial contributor:
 *    Lom M. Hillah - <lom-messan.hillah@lip6.fr>
 *
 *  Mailing list:
 *    lom-messan.hillah@lip6.fr
 */
package fr.lip6.msr4j.asf.utils.parsers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import cern.colt.Arrays;

import fr.lip6.msr4j.asf.datamodel.ASFCommitter;
import fr.lip6.msr4j.asf.datamodel.ASFProject;
import fr.lip6.msr4j.asf.datamodel.ASFProjectsCommitters;
import fr.lip6.msr4j.asf.datamodel.ASFTopLevelProject;
import fr.lip6.msr4j.asf.utils.config.DataSourcesConfig;
import fr.lip6.msr4j.utils.config.ConcurrencyConfig;
import fr.lip6.msr4j.utils.parsers.HTMLPageParser;

/**
 * Parse all data sources from ASF: web pages presenting projects, committers
 * and members, and SVN repositories. Uses Jsoup library to parse the web pages
 * and SVNKit to parse the SVN repositories.
 * 
 * @author lom
 */
public final class ASFCommitterIndexesParser extends
		HTMLPageParser<ASFProjectsCommitters> {
	
	private static final String START_PARSING_MSG = "Start parsing local page at: ";
	private static final double NANO = 1.0e9;
	private static final int ALLOWED_EXEC_TIME10 = 10;
	private static final int ALLOWED_EXEC_TIME100 = 10;
	private static final String HREF = "href";
	private static final String UL_LI = "ul > li";

	public ASFCommitterIndexesParser() {
		super();
	}

	/**
	 * Parses the SVN committers table.
	 * 
	 * @return the placeholder for all projects and their committers.
	 */
	@Override
	public ASFProjectsCommitters parseIndex() {
		ASFProjectsCommitters asfPr = null;
		try {
			logger.info(START_PARSING_MSG
					+ DataSourcesConfig.getCommitterIndexLocal());
			logger.info("Thread Pool Size: " + ConcurrencyConfig.POOL_SIZE);
			// TODO Parse SVN-Committer-index
			long start = System.nanoTime();
			Document doc = loadDocumentFromLocal(
					DataSourcesConfig.getCommitterIndexLocal(),
					DataSourcesConfig.getCommitterIndexBase(),
					DataSourcesConfig.UTF8);
			Element table = doc.getElementsByTag("table").first();
			Element tbody = table.getElementsByTag("tbody").first();
			Elements trs = tbody.getElementsByTag("tr");
			trs.remove(0);

			final List<Callable<Map<ASFCommitter, String[]>>> committerProjPartitions = new ArrayList<Callable<Map<ASFCommitter, String[]>>>();
			for (final Element tr : trs) {
				committerProjPartitions.add(new SVNCommitterIndexParser(tr));
			}
			final ExecutorService executorPool = Executors
					.newFixedThreadPool(ConcurrencyConfig.POOL_SIZE);
			final List<Future<Map<ASFCommitter, String[]>>> values = executorPool
					.invokeAll(committerProjPartitions, ALLOWED_EXEC_TIME10, TimeUnit.SECONDS);
			Map<ASFCommitter, String[]> cs;
			Set<ASFCommitter> cms;
			asfPr = new ASFProjectsCommitters(
					"Apache Software Foundation Projects");
			int howManyProjects = 0;
			int howManyCommitters = 0;
			String[] prj = null;
			for (final Future<Map<ASFCommitter, String[]>> v : values) {
				cs = v.get();
				cms = cs.keySet();
				howManyCommitters += cms.size();
				for (ASFCommitter cm : cms) {
					prj = cs.get(cm);
					asfPr.addProjects(prj, cm);
				}
			}
			long end = System.nanoTime();
			committerProjPartitions.clear();
			logger.info("Finished collecting data from {}. \n\tTime taken: {}",
					DataSourcesConfig.getCommitterIndexLocal(),
					(end - start) / NANO);
			logger.info(
					"Collected {} committers  and {} projects from that page.",
					howManyCommitters, asfPr.getSVNProjects().size() - 1); // void
																			// project
																			// removed
																			// from
																			// the
																			// count.

			// TODO Parse Committers-By-SVNProject
			logger.info(START_PARSING_MSG
					+ DataSourcesConfig.getCommitterBySVNProjectLocal());
			start = System.nanoTime();
			doc = loadDocumentFromLocal(
					DataSourcesConfig.getCommitterBySVNProjectLocal(),
					DataSourcesConfig.getCommitterIndexBase(),
					DataSourcesConfig.UTF8);
			Element content = doc.getElementById("content");
			Elements h2s = content.getElementsByTag("h2");
			Elements tables = content.getElementsByTag("table");
			tables.remove(0);
			// <SVN Project name, <SVN Id, Committer Name>>
			final List<Callable<Map<String, Map<String, String>>>> projCommittersPartitions = new ArrayList<Callable<Map<String, Map<String, String>>>>();
			final Iterator<Element> itH2 = h2s.iterator();
			final Iterator<Element> itTable = tables.iterator();
			if (h2s.size() == tables.size()) {
				Element h2, tab;
				while (itH2.hasNext() && itTable.hasNext()) {
					h2 = itH2.next();
					tab = itTable.next();
					projCommittersPartitions
							.add(new CommittersBySVNProjectParser(h2, tab));
				}
				final List<Future<Map<String, Map<String, String>>>> pcvalues = executorPool
						.invokeAll(projCommittersPartitions, ALLOWED_EXEC_TIME10,
								TimeUnit.SECONDS);
				howManyProjects = 0;
				Map<String, Map<String, String>> projComm = null;
				for (final Future<Map<String, Map<String, String>>> v : pcvalues) {
					projComm = v.get();
					howManyProjects += projComm.keySet().size();
					asfPr.addCommittersBySVNProject(projComm);
				}
				end = System.nanoTime();
				projCommittersPartitions.clear();
				logger.info(
						"Finished collecting SVN Projects data from {}. \n\tTime taken: {}",
						DataSourcesConfig.getCommitterBySVNProjectLocal(),
						(end - start) / NANO);
				logger.info("Number of collected projects from that page: {}.",
						howManyProjects);
				logger.info(
						"Number of new committers: {};  new SVN projects: {}.",
						asfPr.getNewlyCommAdded(), asfPr.getNewlySVNProjAdded());
				asfPr.setNewlyCommAdded(0);
				asfPr.setNewlySVNProjAdded(0);
			} else {
				logger.error("No same number of h2 and table occurrences in the Committers-By-SVNProject document.");
			}

			// TODO - Parse ListofASFCommitters - Voluntary information.
			// Home Page URL (hosted by ASF) can be found in that page.
			logger.info(START_PARSING_MSG
					+ DataSourcesConfig.getCommitterListLocal());
			start = System.nanoTime();
			doc = loadDocumentFromLocal(DataSourcesConfig.getCommitterListLocal(),
					DataSourcesConfig.getCommitterIndexBase(),
					DataSourcesConfig.UTF8);
			final Elements letterSections = doc.getElementById("content")
					.getElementsByClass("letterSection");
			// <SVN Id, [Name, HomePage]>
			final List<Callable<Map<String, String[]>>> commListPartition = new ArrayList<Callable<Map<String, String[]>>>();
			final Iterator<Element> lsIt = letterSections.iterator();
			while (lsIt.hasNext()) {
				commListPartition.add(new CommittersListHomePagesParser(lsIt
						.next()));
			}
			asfPr.setNewlyCommAdded(0);
			final List<Future<Map<String, String[]>>> lsvalues = executorPool
					.invokeAll(commListPartition, 10, TimeUnit.SECONDS);
			howManyCommitters = 0;
			for (final Future<Map<String, String[]>> v : lsvalues) {
				asfPr.addCommittersHomePageById(v.get());
				howManyCommitters += v.get().keySet().size();
			}
			end = System.nanoTime();
			commListPartition.clear();
			logger.info(
					"Finished collecting Committers data from {}. \n\tTime taken: {}",
					DataSourcesConfig.getCommitterListLocal(), (end - start) / NANO);
			logger.info("Number of committers in that page: {}.",
					howManyCommitters);
			logger.info("Number of new committers: {}",
					asfPr.getNewlyCommAdded());

			// TODO - Parse ASF members and Emeritus members and official
			// Projects
			// First encounter of Official top level ASF Projects (# from their
			// SVN
			// projects)
			// This case also presents another opportunity to retrieve home
			// pages url.
			logger.info(START_PARSING_MSG
					+ DataSourcesConfig.getAsfMemberProjectLocal());
			start = System.nanoTime();
			doc = loadDocumentFromLocal(
					DataSourcesConfig.getAsfMemberProjectLocal(),
					DataSourcesConfig.getAsfMemeberIndexBase(),
					DataSourcesConfig.UTF8);
			final Elements memberTables = doc.getElementById("content")
					.getElementsByTag("table");
			// Members <SVN Id, [Name, HomePage]>
			final List<Callable<Map<String, String[]>>> mlistPartition = new ArrayList<Callable<Map<String, String[]>>>();
			mlistPartition.add(new ASFMembersParser(memberTables.get(0)));
			List<Future<Map<String, String[]>>> mvalues = executorPool
					.invokeAll(mlistPartition, ALLOWED_EXEC_TIME10, TimeUnit.SECONDS);
			howManyCommitters = 0;
			for (final Future<Map<String, String[]>> v : mvalues) {
				asfPr.addASFMemberById(v.get());
				howManyCommitters += v.get().keySet().size();
			}
			end = System.nanoTime();
			mlistPartition.clear();
			logger.info(
					"Finished collecting ASF Members from {}. \n\tTime taken: {}",
					DataSourcesConfig.getAsfMemberProjectLocal(),
					(end - start) / NANO);
			logger.info("Number of members in that page: {}.",
					howManyCommitters);
			// Emeritus Members <SVN Id, [Name, HomePage]>
			start = System.nanoTime();
			mlistPartition.add(new ASFMembersParser(memberTables.get(1)));
			mvalues = executorPool.invokeAll(mlistPartition, ALLOWED_EXEC_TIME10,
					TimeUnit.SECONDS);
			howManyCommitters = 0;
			for (final Future<Map<String, String[]>> v : mvalues) {
				asfPr.addASFEmeritusMemberById(v.get());
				howManyCommitters += v.get().keySet().size();
			}
			end = System.nanoTime();
			mlistPartition.clear();
			logger.info(
					"Finished collecting ASF Emeritus Members from {}. \n\tTime taken: {}",
					DataSourcesConfig.getAsfMemberProjectLocal(),
					(end - start) / NANO);
			logger.info("Number of Emeritus members in that page: {}.",
					howManyCommitters);
			// TODO Official Top level ASF Projects
			start = System.nanoTime();
			Elements lis = doc.getElementById("footer").select("div.grid_3")
					.first().select(UL_LI);
			final List<Callable<ASFTopLevelProject>> tlpPartition = new ArrayList<Callable<ASFTopLevelProject>>();
			for (Element li : lis) {
				tlpPartition.add(new ASFTopLevelProjectParser(li));
			}
			List<Future<ASFTopLevelProject>> pvalues = executorPool.invokeAll(
					tlpPartition, ALLOWED_EXEC_TIME10, TimeUnit.SECONDS);
			for (final Future<ASFTopLevelProject> p : pvalues) {
				asfPr.addASFTopLevelProject(p.get());
			}
			end = System.nanoTime();
			tlpPartition.clear();
			logger.info(
					"Finished collecting ASF Top Level Projects from {}. \n\tTime taken: {}",
					DataSourcesConfig.getAsfMemberProjectLocal(),
					(end - start) / NANO);
			logger.info("Number of Top Level Projects in that page: {}.",
					pvalues.size());

			// TODO Parse Projects-Index_Alphabetical
			logger.info(START_PARSING_MSG
					+ DataSourcesConfig.getProjectIndexAlphaLocal());
			start = System.nanoTime();
			doc = loadDocumentFromLocal(
					DataSourcesConfig.getProjectIndexAlphaLocal(),
					DataSourcesConfig.getProjectsIndexBase(), DataSourcesConfig.UTF8);
			Elements sections = doc.getElementById("bodySection")
					.getElementsByClass("body").first()
					.getElementsByClass("section");
			List<Callable<ASFProject>> prPartition = new ArrayList<Callable<ASFProject>>();
			for (Element s : sections) {
				lis = s.select(UL_LI);
				for (Element li : lis) {
					prPartition.add(new ASFProjectIndexAlphaParser(li));
				}
			}
			List<Future<ASFProject>> prvalues = executorPool.invokeAll(
					prPartition, ALLOWED_EXEC_TIME10, TimeUnit.SECONDS);
			for (final Future<ASFProject> p : prvalues) {
				asfPr.addASFProject(p.get());
			}
			end = System.nanoTime();
			prPartition.clear();
			logger.info(
					"Finished collecting ASF Projects from {}. \n\tTime taken: {}",
					DataSourcesConfig.getProjectIndexAlphaLocal(),
					(end - start) / NANO);
			logger.info("Number of Projects in that page: {}.", prvalues.size());

			// TODO - Parse ASFProjects-Index-Listing.html
			logger.info(START_PARSING_MSG
					+ DataSourcesConfig.getProjectIndexListLocal());
			start = System.nanoTime();
			doc = loadDocumentFromLocal(
					DataSourcesConfig.getProjectIndexListLocal(),
					DataSourcesConfig.getProjectsIndexBase(), DataSourcesConfig.UTF8);
			sections = doc.getElementById("bodySection")
					.getElementsByClass("body").first()
					.getElementsByTag("tbody").first().select("tr");
			for (Element tr : sections) {
				prPartition.add(new ASFProjectIndexAlphaParser(tr));
			}
			prvalues = executorPool
					.invokeAll(prPartition, ALLOWED_EXEC_TIME10, TimeUnit.SECONDS);
			for (final Future<ASFProject> p : prvalues) {
				asfPr.checkAddASFProject(p.get());
			}
			end = System.nanoTime();
			prPartition.clear();
			logger.info(
					"Finished collecting ASF Projects from {}. \n\tTime taken: {}",
					DataSourcesConfig.getProjectIndexListLocal(),
					(end - start) / NANO);
			logger.info("Number of Projects in that page: {}.", prvalues.size());

			// TODO - Parse PMC file to extract TLP objects (~ PMC) and relate
			// them to the projects
			logger.info(START_PARSING_MSG
					+ DataSourcesConfig.getProjectIndexPmcLocal());
			start = System.nanoTime();
			doc = loadDocumentFromLocal(DataSourcesConfig.getProjectIndexPmcLocal(),
					DataSourcesConfig.getProjectsIndexBase(), DataSourcesConfig.UTF8);
			sections = doc.getElementById("bodySection")
					.getElementsByClass("body").first()
					.getElementsByClass("section");
			// <TLP Name, Set<Subb projects>>
			List<Callable<Map<String, List<ASFProject>>>> tlprPartition = new ArrayList<Callable<Map<String, List<ASFProject>>>>();
			for (Element s : sections) {
				tlprPartition.add(new ASFProjectPMCParser(s));
			}
			List<Future<Map<String, List<ASFProject>>>> tlprvalues = executorPool
					.invokeAll(tlprPartition, ALLOWED_EXEC_TIME100, TimeUnit.SECONDS);
			int numPr = 0;
			for (final Future<Map<String, List<ASFProject>>> tlpr : tlprvalues) {
				numPr += tlpr.get().values().size();
				asfPr.checkRelatePMCToASFProjects(tlpr.get());
			}
			end = System.nanoTime();
			tlprPartition.clear();
			logger.info(
					"Finished collecting ASF Projects from {}. \n\tTime taken: {}",
					DataSourcesConfig.getProjectIndexPmcLocal(),
					(end - start) / NANO);
			logger.info("Number of TLP Projects in that page: {}.",
					tlprvalues.size());
			logger.info("Number of Projects related to TLPs in that page: {}.",
					numPr);
			// TODO Collect CLAs only (but not yet committers) -
			// To be done later.

			executorPool.shutdown();

		} catch (IOException | InterruptedException | ExecutionException e) {
			logger.error(e.getMessage());
			logger.error(Arrays.toString(e.getStackTrace()));
		}
		return asfPr;
	}

	/**
	 * Parses one line in the table of Committers (from
	 * ASF-SVN-Committer-Index.html). 1 line = committer svn id + committer name
	 * + list of his/her SVN projects
	 */
	private static final class SVNCommitterIndexParser implements
			Callable<Map<ASFCommitter, String[]>> {
		private Element tr;

		public SVNCommitterIndexParser(Element tr) {
			this.tr = tr;
		}

		@Override
		public Map<ASFCommitter, String[]> call() throws Exception {
			final Elements tds = tr.getElementsByTag("td");
			// Parse svnId
			String cid;
			Element b = tds.get(0).getElementsByTag("b").first();
			if (b != null) {
				cid = b.text();
			} else {
				cid = tds.get(0).text();
			}
			// Parse Name
			String cname;
			b = tds.get(1).getElementsByTag("b").first();
			if (b != null) {
				final Element a = b.getElementsByTag("a").first();
				if (a != null) {
					cname = a.text();
				} else {
					cname = b.text();
				}
			} else {
				cname = tds.get(1).text();
			}
			// Parse projects
			String[] cpr;
			final Elements projectRefs = tds.get(2).getElementsByTag("a");
			if (projectRefs != null) {
				cpr = new String[projectRefs.size()];
				for (int i = 0; i < projectRefs.size(); i++) {
					cpr[i] = projectRefs.get(i).text();
				}
			} else {
				cpr = new String[1];
				cpr[0] = "";
			}
			final ASFCommitter cm = new ASFCommitter(cid, cname);
			final Element mem = tds.get(0).getElementsByClass("member").first();
			if (mem != null) {
				cm.setApacheMember(true);
			}
			// All committers in this table has signed the CLA
			cm.setHasSignedCLA(true);
			final Map<ASFCommitter, String[]> result = new HashMap<ASFCommitter, String[]>();
			result.put(cm, cpr);
			return result;
		}
	}

	/**
	 * Parses one table in the file ASF-Committers-by-SVNProject. A table holds
	 * information about one SVN project : the committers's ids and their names.
	 * It is preceded by a H2 tag containing the SVN project name
	 * 
	 * @return a Map containing a SVN project name, associated with its maps of
	 *         Committers ids and names.
	 * @author lom
	 * 
	 */
	private static final class CommittersBySVNProjectParser implements
			Callable<Map<String, Map<String, String>>> {
		private final Element h2, tab;

		public CommittersBySVNProjectParser(Element h2, Element tab) {
			this.h2 = h2;
			this.tab = tab;
		}

		@Override
		public Map<String, Map<String, String>> call() throws Exception {
			String svnPrName = h2.text().trim();
			final Elements trs = tab.getElementsByTag("tr");
			trs.remove(0);
			String svnId = null, name = null;
			Element td, b, a;
			final Map<String, String> committer = new HashMap<String, String>();
			for (Element tr : trs) {
				svnId = tr.getElementsByTag("td").first().getElementsByTag("a")
						.first().text();
				td = tr.getElementsByTag("td").get(1);
				b = td.getElementsByTag("b").first();
				if (b != null) {
					a = b.getElementsByTag("a").first();
					if (a != null) {
						name = a.text();
					} else {
						name = b.text();
					}
				} else {
					a = td.getElementsByTag("a").first();
					if (a != null) {
						name = a.text();
					} else {
						name = td.text();
					}
				}
				committer.put(svnId, name);
			}
			final Map<String, Map<String, String>> result = new HashMap<String, Map<String, String>>();
			result.put(svnPrName, committer);
			return result;
		}
	}

	/**
	 * Parses one letter section in the file ListofASFCommitters.html. Main
	 * interesting information to retrieve from that page is the committer's
	 * home page URL Map location URL can be deduced from a pattern, but actual
	 * geo location retrieval is not yet performed (haven't fully investigated
	 * the issue yet).
	 * 
	 * @author lom
	 * 
	 */
	private static final class CommittersListHomePagesParser implements
			Callable<Map<String, String[]>> {

		
		private final Element letterSection;

		public CommittersListHomePagesParser(Element el) {
			this.letterSection = el;
		}

		@Override
		public Map<String, String[]> call() throws Exception {
			final Map<String, String[]> result = new HashMap<String, String[]>();
			String svnId, name, hp;
			Element img;
			final Elements lis = this.letterSection.select(UL_LI);
			for (Element li : lis) {
				svnId = li.getElementsByTag("a").first().attr(HREF)
						.split("#")[1];
				name = li.getElementsByTag("a").first().text();
				img = li.select("a > img[title=Homepage]").first();
				if (img != null) {
					hp = img.parent().attr(HREF);
				} else {
					hp = null;
				}
				String[] cmInfo = { name, hp };
				result.put(svnId, cmInfo);
			}
			return result;
		}
	}

	/**
	 * Parses ASF Members table in the file ASFMemebersNProjects.html
	 * 
	 * @author lom
	 * 
	 */
	private static final class ASFMembersParser implements
			Callable<Map<String, String[]>> {

		private final Element table;

		public ASFMembersParser(Element t) {
			this.table = t;
		}

		@Override
		public Map<String, String[]> call() throws Exception {
			final Map<String, String[]> result = new HashMap<String, String[]>();
			String svnId, name, hp;
			Element td, a;
			final Elements trs = this.table.select("tbody > tr");
			for (Element tr : trs) {
				svnId = tr.getElementsByTag("td").first().text();
				td = tr.getElementsByTag("td").get(1);
				a = td.select("a").first();
				if (a != null) {
					hp = a.attr(HREF);
					name = a.text();
				} else {
					name = td.text();
					hp = null;
				}
				String[] membInfo = { name, hp };
				result.put(svnId, membInfo);
			}
			return result;
		}
	}

	/**
	 * Parses ASF Top Level Project (name, URL and title (short description))
	 * 
	 * @author lom
	 * 
	 */
	private static final class ASFTopLevelProjectParser implements
			Callable<ASFTopLevelProject> {
		private final Element li;

		public ASFTopLevelProjectParser(Element li) {
			this.li = li;
		}

		@Override
		public ASFTopLevelProject call() throws Exception {
			final Element a = li.getElementsByTag("a").first();
			final ASFTopLevelProject result = new ASFTopLevelProject(a.text(),
					a.attr(HREF), a.attr("title"));
			return result;
		}
	}

	/**
	 * Parses ASF projects index in Alpha order
	 * (ASFProjects-Index_Alphabetical.html)
	 * 
	 * @author lom
	 * 
	 */
	private static final class ASFProjectIndexAlphaParser implements
			Callable<ASFProject> {
		private final Element li;

		public ASFProjectIndexAlphaParser(Element li) {
			this.li = li;
		}

		@Override
		public ASFProject call() throws Exception {
			Element a = li.getElementsByTag("a").first();
			String name = a.text();
			String url = a.attr(HREF);
			String desc = li.select("div.smallitalic").first().text();
			Element br = li.select("div.smallplain").first().select("br")
					.first();
			a = br.previousElementSibling();
			List<String> categories = new ArrayList<String>();
			// Read categories in reverse order
			while (a != null && a.tagName().equalsIgnoreCase("a")) {
				categories.add(a.text());
				a = a.previousElementSibling();
			}
			br = li.select("div.smallplain").first().select("br").get(1);
			List<String> languages = new ArrayList<String>();
			a = br.previousElementSibling();
			// Read languages in reverse order
			while (a != null && !a.tagName().equalsIgnoreCase("br")) {
				languages.add(a.text());
				a = a.previousElementSibling();
			}
			// Read PMC info
			a = li.select("div.smallplain").first().select("a").last();
			String pmc = null;
			if (a.tagName().equalsIgnoreCase("a")) {
				pmc = a.text();
			}
			ASFProject result = new ASFProject(name, url, desc);
			result.setCategories(categories.toArray(new String[0]));
			result.setLanguages(languages.toArray(new String[0]));
			result.setPmc(pmc);
			return result;
		}
	}

	/**
	 * Parses PMC index and relates a Top-level project to its sub-projects.
	 * 
	 * @author lom
	 * 
	 */
	private static final class ASFProjectPMCParser implements
			Callable<Map<String, List<ASFProject>>> {

		private final Element section;

		public ASFProjectPMCParser(Element s) {
			this.section = s;
		}

		@Override
		public Map<String, List<ASFProject>> call() throws Exception {
			// Initially we didn't parse TLPs with the pattern "Apache " in
			// their names.
			// So we remove them.
			String tlpname = this.section.getElementsByTag("h3").first()
					.ownText().replaceFirst("Apache ", "");
			Elements lis = this.section.select(UL_LI);
			List<Callable<ASFProject>> prPartition = new ArrayList<Callable<ASFProject>>();
			for (Element li : lis) {
				prPartition.add(new ASFProjectIndexAlphaParser(li));
			}
			final ExecutorService executorPool = Executors
					.newCachedThreadPool();
			List<Future<ASFProject>> prvalues = executorPool.invokeAll(
					prPartition, ALLOWED_EXEC_TIME10, TimeUnit.SECONDS);
			List<ASFProject> prs = new ArrayList<ASFProject>();
			for (final Future<ASFProject> p : prvalues) {
				prs.add(p.get());
			}
			executorPool.shutdown();
			Map<String, List<ASFProject>> result = new HashMap<String, List<ASFProject>>();
			result.put(tlpname, prs);
			return result;
		}
	}
}
